package com.virtual.repository.common.criteria;

import java.util.Collection;
import java.util.LinkedList;
import java.util.List;

import com.virtual.repository.common.operators.CriteriaOperator;
import com.virtual.repository.common.operators.LogicalOperator;

/**
 * A builder for lists of criteria to be used in statements. Ensures the syntactical correctness of the criteria.
 * @author Zielony
 * @version 1.0
 */
public class CriteriaBuilder implements ICriteriaBuilder {
	/*
	 * Linked list used to enable cloning.
	 */
	/**
	 * An intermediate list held while building the criteria.
	 */
	private LinkedList<Criterion> criteria = new LinkedList<Criterion>(); 
	
	/**
	 * Adds a <code>Criterion</code> using the "IN" or "NOT_IN" operator without a logical prefix operator.
	 * @param property the property on which the criterion is set.
	 * @param valueSet the set of values for the "IN" or "NOT IN" operator.
	 * @param negation whether the "IN" operator should be negated.
	 */
	public void addInCollectionCriterion(String property, Collection<?> valueSet, boolean negation) {
		this.addInCollectionCriterion(null, property, valueSet, negation);
	}
	
	/**
	 * Adds a <code>Criterion</code> using the "IN" or "NOT_IN" operator without a logical prefix operator.
	 * @param prefixOperator the logical prefix operator connecting the criterion with the preceding one.
	 * @param property the property on which the criterion is set.
	 * @param valueSet the set of values for the "IN" or "NOT IN" operator.
	 * @param negation whether the "IN" operator should be negated.
	 */
	public void addInCollectionCriterion(LogicalOperator prefixOperator, String property, Collection<?> valueSet, boolean negation) {
		/*
		 * The property name cannot be null.
		 */
		if(property == null) {
			throw new NullPointerException("Property name on the CriteriaBuilder entry is null.");
		}
		
		/*
		 * The value set cannot be null. 
		 */
		if(valueSet == null) {
			throw new NullPointerException("Value set on the CriteriaBuilder entry is null.");
		}
		
		/*
		 * Checking if the prefix operator is correct in the current context. 
		 */
		if(! isPrefixOperatorCorrect(prefixOperator)) {
			throw new IllegalStateException("Incorrect prefix operator for the current state");
		}
		
		/*
		 * Selecting the "IN" or "NOT IN" operator as stated. Building and adding the criterion. 
		 */
		if(negation) {
			criteria.add(new Criterion(prefixOperator, property, CriteriaOperator.NOT_IN, valueSet));
		}
		else {
			criteria.add(new Criterion(prefixOperator, property, CriteriaOperator.IN, valueSet));
		}
	}
	
	/**
	 * Adds a new RegEx-based <code>Criterion</code> without a prefix operator.
	 * @param property the property on which the criterion is set.
	 * @param regEx the RegEx to which the property is compared.
	 */
	public void addRegexCriterion(String property, String regEx) {
		this.addRegexCriterion(null, property, regEx);
	}
	
	/**
	 * Adds a new RegEx-based <code>Criterion</code> without a prefix operator.
	 * @param prefixOperator the logical prefix operator connecting the criterion with the preceding one.
	 * @param property the property on which the criterion is set.
	 * @param regEx the RegEx to which the property is compared.
	 */
	public void addRegexCriterion(LogicalOperator prefixOperator, String property, String regEx) {
		/*
		 * The property name cannot be null. 
		 */
		if(property == null) {
			throw new NullPointerException("Property name on the CriteriaBuilder entry is null.");
		}
		
		/*
		 * Checking the prefix operator correctness in the current context.
		 */
		if(! isPrefixOperatorCorrect(prefixOperator)) {
			throw new IllegalStateException("Incorrect prefix operator for the current state");
		}
		
		criteria.add(new Criterion(property, CriteriaOperator.LIKE, regEx));
	}
	
	/* (non-Javadoc)
	 * @see com.virtual.repository.common.criteria.ICriteriaBuilder#addCriterion(java.lang.String, com.virtual.repository.common.operators.CriteriaOperator, java.lang.Object)
	 */
	@Override
	public void addCriterion(String property, CriteriaOperator operator, Object value) {
		this.addCriterion(null, property, operator, value);
	}
	
	/* (non-Javadoc)
	 * @see com.virtual.repository.common.criteria.ICriteriaBuilder#addCriterion(com.virtual.repository.common.operators.LogicalOperator, java.lang.String, com.virtual.repository.common.operators.CriteriaOperator, java.lang.Object)
	 */
	@Override
	public void addCriterion(LogicalOperator prefixOperator, String property, CriteriaOperator operator, Object value ) {
		/*
		 * The property name cannot be null. 
		 */
		if(property == null) {
			throw new NullPointerException("Property name on the CriteriaBuilder entry is null.");
		}
		
		/*
		 * Checking the prefix operator correctness in the current context. 
		 */
		if(! isPrefixOperatorCorrect(prefixOperator)) {
			throw new IllegalStateException("Incorrect prefix operator for the current state");
		}
	
		/*
		 * Checking whether the operator is used with a correct value type. 
		 */
		if(!isOperatorValuePairCorrect(operator, value)) {
			throw new IllegalArgumentException("Operator - value type mismatch");
		}
		
		criteria.add(new Criterion(prefixOperator, property, operator, value));
	}
	
	/**
	 * Checks the <code>prefixOperator</code> correctness in the current context. Namely, an opening criterion in any
	 * criteria set should have a <code>null</code> prefixOperator, while any following should have a non-null prefix
	 * operator.
	 * @param prefixOperator the prefix operator in question.
	 * @return <code>true</code> if the <code>prefixOperator</code> is used correctly, <code>false</code> if otherwise.
	 */
	public boolean isPrefixOperatorCorrect(LogicalOperator prefixOperator) {
		/*
		 * If theere are no criteria yet, the prefix operator should be null. 
		 */
		if(criteria.size() == 0 && prefixOperator != null  ) {
			return false;
		}
		
		/*
		 * If there are criteria already, the prefix operator should not be null.
		 */
		if(criteria.size() != 0 && prefixOperator == null) {
			return false;
		}
		
		return true;
	}
	
	/**
	 * Checks if the <code>operator</code> matches the given <code>value</code> type. Namely, the "IN" and "NOT IN"
	 * operators should only be used with non-null <code>Collection</code>s, the "LIKE" and "NOT LIKE" operators
	 * are only to be used with <code>String</code>s and the "GREATER" , "LESSER", "GREATER OR EQUAL" and
	 * "LESSER OR EQUAL" operators are to be used with number-representing types only for the <code>java.lang</code>
	 * package only.
	 * @param operator the operator in question.
	 * @param value the value in question. 
	 * @return <code>true</code> if the value type matches the operator, <code>false</code> otherwise.
	 */
	public boolean isOperatorValuePairCorrect(CriteriaOperator operator, Object value) {
		/*
		 * The "IN" and "NOT IN" operators should only be used with non-null Collections.
		 */
		if((operator.equals(CriteriaOperator.IN) || operator.equals(CriteriaOperator.NOT_IN) ) &&
				!(value instanceof Collection)) {
			if(value == null) {
				return false;
			}
			
			if(!(value instanceof Collection)) {
				return false;
			}
		}
		
		/*
		 * the "LIKE" and "NOT LIKE" operators are only to be used with Strings.
		 */
		if(( operator.equals(CriteriaOperator.LIKE) || operator.equals(CriteriaOperator.NOT_LIKE) ) && 
				!value.getClass().equals(String.class)) {
			return false;
		}
		
		Class<?> valueClass = value.getClass();
		
		/*
		 * The "GREATER" , "LESSER", "GREATER OR EQUAL" and "LESSER OR EQUAL" operators are to be used with 
		 * number-representing types only for the java.lang package only.
		 */
		if(operator.equals(CriteriaOperator.GRATER) || operator.equals(CriteriaOperator.GRATER_OR_EQUAL) || 
				operator.equals(CriteriaOperator.LESSER) || operator.equals(CriteriaOperator.LESSER_OR_EQUAL)) {
		
			
			if(valueClass.equals(int.class) || valueClass.equals(Integer.class) || valueClass.equals(float.class) ||
					valueClass.equals(Float.class) || valueClass.equals(double.class) ||
					valueClass.equals(Double.class) || valueClass.equals(short.class) ||
					valueClass.equals(Short.class) || valueClass.equals(byte.class) || 
					valueClass.equals(Byte.class) ) {
					  ;	
					}
			else {
				return false;
			}
		}
		
		return true;
	}
	
	/* (non-Javadoc)
	 * @see com.virtual.repository.common.criteria.ICriteriaBuilder#build()
	 */
	@Override
	public List<Criterion> build() throws CloneNotSupportedException {
		/*
		 * Bulding the criteria list and returning its defensive copy. 
		 */
		return ((List<Criterion>)criteria.clone());
	}
}